package main

import (
	"bytes"
	"fmt"
	"go/token"
	"os"
	"strings"
	"text/template"

	transformers "github.com/nucleuscloud/neosync/worker/pkg/benthos/transformers"
)

func main() {
	args := os.Args
	if len(args) < 1 {
		panic("must provide necessary args")
	}

	packageName := args[1]
	fileSet := token.NewFileSet()

	transformerFuncs, err := transformers.ExtractBenthosSpec(fileSet)
	if err != nil {
		fmt.Println("Error finding transformer bloblang specs:", err) //nolint:forbidigo
		return
	}

	for _, tf := range transformerFuncs {
		p, err := transformers.ParseBloblangSpec(tf)
		if err != nil {
			fmt.Println("Error parsing bloblang params:", err)
			return
		}
		tf.Params = p.Params
		tf.Description = p.SpecDescription
		tf.BloblangFuncName = p.BloblangFuncName
		tf.Category = p.Category
	}

	for _, tf := range transformerFuncs {
		codeStr, err := generateCode(packageName, tf)
		if err != nil {
			fmt.Println("Error writing to output file:", err)
			return
		}
		output := fmt.Sprintf("gen_%s", tf.SourceFile)
		outputFile, err := os.Create(output)
		if err != nil {
			fmt.Println("Error creating output file:", err)
			return
		}

		_, err = outputFile.WriteString(codeStr)
		if err != nil {
			fmt.Println("Error writing to output file:", err)
			return
		}
		outputFile.Close()
	}

	// generate transformer benchmarks
	benchmarkCode, err := generateBenchmarkCode(packageName, transformerFuncs)
	if err != nil {
		fmt.Println("Error generating benchmarks:", err)
		return
	}
	outputFile, err := os.Create("benchmarks_test.go")
	if err != nil {
		fmt.Println("Error creating benchmarks file:", err)
		return
	}

	_, err = outputFile.WriteString(benchmarkCode)
	if err != nil {
		fmt.Println("Error writing to benchmarks file:", err)
		return
	}
	outputFile.Close()
}

const codeTemplate = `
// Code generated by Neosync neosync_transformer_generator.go. DO NOT EDIT.
// source: {{.SourceFile}}

package {{.PackageName}}

import (
	"strings"
	"fmt"
	{{- if eq .HasSeedParam true}}
	transformer_utils "github.com/nucleuscloud/neosync/worker/pkg/benthos/transformers/utils"
	"github.com/nucleuscloud/neosync/worker/pkg/rng"
	{{ end }}
)

type {{.StructName}} struct{}

type {{.StructName}}Opts struct {
	{{- if eq .HasSeedParam true}}
	randomizer     rng.Rand
	{{ end }}
	{{- range $index, $param := .FunctInfo.Params }}
	{{- if eq $param.Name "value" }}{{ continue }}{{ end }}
	{{- if eq $param.Name "seed" }}{{ continue }}{{ end }}
	{{- if $param.IsOptional }}
	{{$param.Name}} *{{$param.TypeStr}}
	{{- else }}
	{{$param.Name}} {{$param.TypeStr}}
	{{- end }}
	{{- end }}
}

func New{{.StructName}}() *{{.StructName}} {
	return &{{.StructName}}{}
}

func New{{.StructName}}Opts(
{{- range $index, $param := .FunctInfo.Params }}
	{{- if eq $param.Name "value" }}{{ continue }}{{ end }}
	{{- if eq $param.Name "seed"}}
  {{$param.Name}}Arg *{{$param.TypeStr}},
	{{- else if and $param.IsOptional (not $param.HasDefault) }}
	{{$param.Name}} *{{$param.TypeStr}},
	{{- else if or $param.IsOptional $param.HasDefault }}
	{{$param.Name}}Arg *{{$param.TypeStr}},
	{{- else }}
	{{$param.Name}} {{$param.TypeStr}},
	{{- end }}
{{- end }}
) (*{{.StructName}}Opts, error) {
{{- range $index, $param := .FunctInfo.Params }}
 	{{- if eq $param.Name "value" }}{{ continue }}{{ end }}
 	{{- if eq $param.Name "seed" }}
	seed, err := transformer_utils.GetSeedOrDefault(seedArg)
  if err != nil {
    return nil, fmt.Errorf("unable to generate seed: %w", err)
	}
	{{ else if $param.HasDefault }}
	{{- if eq $param.TypeStr "any" }}
	var {{$param.Name}} any
	{{- else}}
	{{$param.Name}} := {{$param.TypeStr}}({{$param.Default}})
	{{- end }}
	if {{$param.Name}}Arg != nil {
		{{$param.Name}} = *{{$param.Name}}Arg
	}
	{{ end }}
{{- end }}
	return &{{.StructName}}Opts{
{{- range $index, $param := .FunctInfo.Params }}
    {{- if eq $param.Name "value" }}{{ continue }}{{ end }}
		{{- if eq $param.Name "seed" }}
		randomizer: rng.New(seed),
		{{- else }}
		{{$param.Name}}: {{$param.Name}},
    {{- end }}
{{- end }}
	}, nil
}

func (o *{{.StructName}}Opts) BuildBloblangString(
{{- if .IsTransformer }}
	valuePath string,
{{- end }}
) string {
	fnStr := []string{
	{{- range $index, $param := .FunctInfo.Params }}
	{{- if eq $param.Name "seed" }}{{ continue }}{{ end }}
	{{- if eq $param.Name "value" }}
		"value:this.%s",
	{{- else if not $param.IsOptional }}
		"{{$param.BloblangName}}:{{- if eq $param.TypeStr "string" }}%q{{else}}%v{{end}}",
	{{- end }}
	{{- end }}
	}

	params := []any{
	{{- range $index, $param := .FunctInfo.Params }}
	{{- if eq $param.Name "seed" }}{{ continue }}{{ end }}
	{{- if eq $param.Name "value" }}
		valuePath,
	{{- else if not $param.IsOptional }}
	 	o.{{$param.Name}},
	{{- end }}
	{{- end }}
	}

	{{ range $index, $param := .FunctInfo.Params }}
	{{- if eq $param.Name "value" }}{{ continue }}{{ end }}
	{{- if eq $param.Name "seed" }}{{ continue }}{{ end }}
	{{- if $param.IsOptional }}
	if o.{{$param.Name}} != nil {
		fnStr = append(fnStr, "{{$param.BloblangName}}:{{- if eq $param.TypeStr "string" }}%q{{else}}%v{{end}}")
		params = append(params, *o.{{$param.Name}})
	}
	{{- end -}}
	{{- end }}

	template := fmt.Sprintf("{{ .FunctInfo.BloblangFuncName }}(%s)", strings.Join(fnStr, ","))
	return fmt.Sprintf(template, params...)
}

func (t *{{.StructName}}) GetJsTemplateData() (*TemplateData, error) {
	return &TemplateData{
		Name: "{{.FunctInfo.Name}}",
		Description: "{{.FunctInfo.Description}}",
		Example: "{{.FunctInfo.Example}}",
	}, nil
}

func (t *{{.StructName}}) ParseOptions(opts map[string]any) (any, error) {
	transformerOpts := &{{.StructName}}Opts{}
	{{- range $index, $param := .FunctInfo.Params }}
	{{- if eq $param.Name "value" }}{{ continue }}{{ end }}

	{{- if eq $param.Name "seed" }}

	var seedArg *int64
	if seedValue, ok := opts["seed"].(int64); ok {
			seedArg = &seedValue
	}
	seed, err := transformer_utils.GetSeedOrDefault(seedArg)
	if err != nil {
		return nil, fmt.Errorf("unable to generate seed: %w", err)
	}
	transformerOpts.randomizer = rng.New(seed)

	{{- continue }}
	{{ end }}
	{{- if $param.HasDefault }}

	{{$param.Name}}, ok := opts["{{$param.Name}}"].({{$param.TypeStr}})
	if !ok {
		{{$param.Name}} = {{$param.Default}}
	}

	{{- else if $param.IsOptional }}

	var {{$param.Name}} *{{$param.TypeStr}}
	if arg, ok := opts["{{$param.Name}}"].({{$param.TypeStr}}); ok {
		{{$param.Name}} = &arg
	}

	{{- else }}

	if _, ok := opts["{{$param.Name}}"].({{$param.TypeStr}}); !ok {
		return nil, fmt.Errorf("missing required argument. function: %s argument: %s", "{{ $.FunctInfo.Name }}", "{{$param.Name}}")
	}
	{{$param.Name}} := opts["{{$param.Name}}"].({{$param.TypeStr}})

	{{- end }}
	transformerOpts.{{$param.Name}} = {{$param.Name}}
	{{- end }}

	return transformerOpts, nil
}
`

type TemplateData struct {
	SourceFile    string
	PackageName   string
	FunctInfo     transformers.BenthosSpec
	StructName    string
	HasSeedParam  bool
	IsTransformer bool
}

func generateCode(pkgName string, funcInfo *transformers.BenthosSpec) (string, error) {
	data := TemplateData{
		SourceFile:  funcInfo.SourceFile,
		PackageName: pkgName,
		FunctInfo:   *funcInfo,
		StructName:  capitalizeFirst(funcInfo.Name),
	}
	for _, p := range funcInfo.Params {
		if p.Name == "seed" {
			data.HasSeedParam = true
		}
		if p.Name == "value" {
			data.IsTransformer = true
		}
	}
	t := template.Must(template.New("neosyncTransformerImpl").Parse(codeTemplate))
	var out bytes.Buffer
	err := t.Execute(&out, data)
	if err != nil {
		return "", err
	}
	return out.String(), nil
}

func capitalizeFirst(s string) string {
	if s == "" {
		return s
	}
	return strings.ToUpper(string(s[0])) + s[1:]
}

var testValues = map[string]any{
	"string":  `"test"`,
	"int64":   123456,
	"float64": 123.45,
	"boolean": true,
	"email":   `"test@test.com"`,
}

const benchmarkTemplate = `
// Code generated by Neosync neosync_transformer_generator.go. DO NOT EDIT.

package {{.PackageName}}

import (
	"testing"
)
{{ $testValues := .TestValues }}
{{- range $index, $spec := .TransformerSpecs }}
{{- if eq $spec.Type "generate" }}
func Benchmark{{$spec.Name}}(b *testing.B) {
	generator := New{{$spec.Name}}()
	opts, err := New{{$spec.Name}}Opts(
	{{- range $index, $param := $spec.Params }}
	{{- if eq $param.Name "value" }}{{ continue }}{{ end -}}
	nil,
	{{- end -}}
	)
	if err != nil {
		b.Fatal(err)
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := generator.Generate(opts)
		if err != nil {
			b.Fatal(err)
		}
	}
}
{{- end }}

{{ if eq $spec.Type "transform" }}
func Benchmark{{$spec.Name}}(b *testing.B) {
	transformer := New{{$spec.Name}}()
	opts, err := New{{$spec.Name}}Opts(
	{{- range $index, $param := $spec.Params }}
	{{- if eq $param.Name "value" }}{{ continue }}{{ end -}}
	nil,
 {{- end -}}
	)
	if err != nil {
		b.Fatal(err)
	}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := transformer.Transform({{index $testValues $spec.Category}}, opts)
		if err != nil {
			b.Fatal(err)
		}
	}
}
{{- end }}
{{- end }}
`

type BenchmarkTemplateData struct {
	// SourceFile    string
	TestValues       map[string]any
	PackageName      string
	TransformerSpecs []*transformers.BenthosSpec
	StructName       string
}

func generateBenchmarkCode(pkgName string, transformerSpecs []*transformers.BenthosSpec) (string, error) {
	for _, tf := range transformerSpecs {
		tf.Name = capitalizeFirst(tf.Name)
	}

	data := BenchmarkTemplateData{
		PackageName:      pkgName,
		TransformerSpecs: transformerSpecs,
		TestValues:       testValues,
	}

	t := template.Must(template.New("neosyncTransformerBenchmarkImpl").Parse(benchmarkTemplate))
	var out bytes.Buffer
	err := t.Execute(&out, data)
	if err != nil {
		return "", err
	}
	return out.String(), nil
}
